package be.sc.classicnetwork.dao.hibernate;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.persistence.ManyToMany;
import javax.persistence.OneToMany;

import org.apache.log4j.Logger;
import org.hibernate.FetchMode;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.springframework.dao.support.DataAccessUtils;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
import org.springframework.util.Assert;

import be.sc.classicnetwork.dao.GenericDao;
import be.sc.classicnetwork.exception.ApplicationException;
import be.sc.classicnetwork.model.AbstractEntity;

/**
 * Implementation of the Generic DAO interface using Hibernate OR mapper with
 * CRUD functionalities and contains some helpful methods for DAOs inheriting
 * from this class.
 */
public class HibernateGenericDao<T extends AbstractEntity> extends HibernateDaoSupport implements GenericDao<T> {

    /** the logger */
    protected final Logger log = Logger.getLogger(this.getClass());

    private Class<T> clazz;

    /**
     * Constructor.
     */
    public HibernateGenericDao() {
	super();
    }

    /**
     * Constructor.
     */
    public HibernateGenericDao(Class<T> clazz) {
	super();
	this.clazz = clazz;
    }

    /**
     * @see be.sc.classicnetwork.dao.GenericDao#findById(int)
     */
    @SuppressWarnings("unchecked")
    public T findById(String id) {
	Assert.hasText(id);

	log.debug("Finding " + clazz.getName() + " with id = " + id);
	return (T) getHibernateTemplate().get(clazz, id);
    }

    /**
     * Gets the field names having an Annotation <code>@OneToMany</code> or <code>@ManyToMany</code>.
     */
    private String[] getAnyToManyFields(Class<? extends AbstractEntity> clazz) {
	Assert.notNull(clazz);

	// get all fields of the class
	Field[] allFields;
	try {
	    allFields = clazz.getDeclaredFields();
	} catch (SecurityException e) {
	    throw new ApplicationException(e);
	}

	// get the fields having a @OneToMany
	ArrayList<String> anyToManyAnnotatedFields = new ArrayList<String>(allFields.length);
	for (Field f : allFields) {
	    if ((f.getAnnotation(OneToMany.class) != null) || (f.getAnnotation(ManyToMany.class) != null)) {
		anyToManyAnnotatedFields.add(f.getName());
	    }
	}

	return anyToManyAnnotatedFields.toArray(new String[0]);
    }

    /**
     * @see be.sc.classicnetwork.dao.GenericDao#findByIdWithFullDependencies(int)
     */
    @SuppressWarnings("unchecked")
    public T findByIdWithFullDependencies(String id) {
	Assert.hasText(id);
	log.debug("Finding " + clazz.getName() + " with full dependencies loaded with id = " + id);

	// get the fields having a @OneToMany and fetch them
	String[] anyToManyFields = getAnyToManyFields(clazz);

	return findByIdWithDependencies(id, anyToManyFields);
    }

    /**
     * @see be.sc.classicnetwork.dao.GenericDao#findByIdWithDependencies(java.lang.String,
     *      java.lang.String[])
     */
    @SuppressWarnings("unchecked")
    public T findByIdWithDependencies(String id, String... dependencies) {
	Assert.hasText(id);
	log.debug("Finding " + clazz.getName() + " with specified dependencies loaded: " + dependencies + " with id = "
		+ id);

	// load the specified dependencies
	DetachedCriteria criteria = DetachedCriteria.forClass(clazz);
	for (String f : dependencies) {
	    criteria.setFetchMode(f, FetchMode.JOIN);
	}

	// add id criterion
	criteria.add(Restrictions.idEq(id));

	return (T) DataAccessUtils.uniqueResult(getHibernateTemplate().findByCriteria(criteria));
    }

    /**
     * @see be.sc.classicnetwork.dao.GenericDao#findAll(int, int)
     */
    @SuppressWarnings("unchecked")
    public List<T> findAll(int first, int max) {
	Assert.state(first >= 0 && max >= 0 && max >= first);
	log.debug("Finding all " + clazz.getName());
	return findByCriteria(first, max, null, new Criterion[0], null);
    }

    /**
     * @see be.sc.classicnetwork.dao.GenericDao#saveOrUpdate(be.sc.classicnetwork.model.AbstractEntity)
     */
    public T saveOrUpdate(T entity) {
	Assert.notNull(entity);
	log.debug("Saving " + clazz.getName() + " with id = " + entity.getId());
	getHibernateTemplate().saveOrUpdate(entity);
	return entity;
    }

    /**
     * @see be.sc.classicnetwork.dao.GenericDao#saveOrUpdateAny(java.lang.Object)
     */
    public Object saveOrUpdateAny(Object o) {
	Assert.notNull(o);
	log.debug("Saving " + o.getClass().getCanonicalName());
	getHibernateTemplate().saveOrUpdate(o);
	return o;
    }

    /**
     * @see be.sc.classicnetwork.dao.GenericDao#delete(be.sc.classicnetwork.model.AbstractEntity)
     */
    public void delete(T entity) {
	Assert.notNull(entity);
	log.debug("Deleting " + clazz.getName() + " with id = " + entity.getId());
	getHibernateTemplate().delete(entity);
    }

    /**
     * @see be.sc.classicnetwork.dao.GenericDao#deleteAll()
     */
    public void deleteAll() {
	log.debug("Deleting all " + clazz.getName());
	getHibernateTemplate().deleteAll(findAll(0, 0));
    }

    /**
     * @see be.sc.classicnetwork.dao.GenericDao#countAll()
     */
    public int countAll() {
	log.debug("Counting all " + clazz.getName());
	return countByCriteria(null, new Criterion[0]);
    }

    /**
     * @see be.sc.classicnetwork.dao.GenericDao#findByIdIn(java.lang.Integer[])
     */
    public List<T> findByIdIn(String... ids) {
	Assert.notEmpty(ids);
	return findByCriteria(0, 0, null, new Criterion[] { Restrictions.in("id", ids) }, null);
    }

    /**
     * Gets a DetachedCriteria from a set of Criterion.
     * 
     * @param criteria
     * @return
     */
    private DetachedCriteria getCriteria(Map<String, String> aliases, Criterion[] criteria, Order[] orders) {
	DetachedCriteria detachedCriteria = DetachedCriteria.forClass(clazz);

	// set the aliases if any
	if (aliases != null && !aliases.isEmpty()) {
	    for (Map.Entry<String, String> alias : aliases.entrySet()) {
		detachedCriteria.createAlias(alias.getKey(), alias.getValue());
	    }
	}

	// set criteria
	if (criteria != null && criteria.length > 0) {
	    for (Criterion c : criteria) {
		detachedCriteria.add(c);
	    }
	}

	// set the order by clause(s) if any
	if (orders != null && orders.length > 0) {
	    for (Order order : orders) {
		detachedCriteria.addOrder(order);
	    }
	}

	return detachedCriteria;
    }

    /**
     * findByCriteria.
     */
    @SuppressWarnings("unchecked")
    protected List<T> findByCriteria(int first, int max, Map<String, String> aliases, Criterion[] criteria,
	    Order[] orders) {
	Assert.state(first >= 0 && max >= 0 && max >= first);
	log.debug("Finding " + clazz.getName() + " by criteria");
	return getHibernateTemplate().findByCriteria(getCriteria(aliases, criteria, orders), first, max);
    }

    /**
     * Counts the entities matching the specified criteria.
     * 
     * @param criteria
     * @return
     */
    protected int countByCriteria(Map<String, String> aliases, Criterion... criteria) {
	log.debug("Counting " + clazz.getName() + " by criteria");
	DetachedCriteria detachedCriteria = getCriteria(aliases, criteria, null);

	// set count projection
	detachedCriteria.setProjection(Projections.rowCount());

	return DataAccessUtils.intResult(getHibernateTemplate().findByCriteria(detachedCriteria));
    }

    public Class<T> getClazz() {
	return clazz;
    }

    public void setClazz(Class<T> clazz) {
	this.clazz = clazz;
    }
}
